/**
 * Copyright 2000-2011 Worth Enterprises, Inc.  All rights reserved.
 */
package com.worthent.foundation.util.state.provider;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import com.worthent.foundation.util.state.StateDef;
import com.worthent.foundation.util.state.StateEvent;
import com.worthent.foundation.util.state.StateExeException;
import com.worthent.foundation.util.state.StateTransitionDef;

/**
 * Wraps a State Table Control object with an implementation of a SAX Default
 * Handler. Construct an instance of this object on a State Table Control object
 * and then pass the instances as the Default Handler in the SAX Parser's parse
 * method.
 * 
 * @author Erik K. Worth
 * @version $Id: SaxEventAdapter.java 2 2011-11-28 00:10:06Z erik.k.worth@gmail.com $
 */
public class SaxEventAdapter extends DefaultHandler {

    /** Event name for the Start Document notification */
    public static final String START_DOCUMENT = "StartDocument";

    /** Event name for the End Document notification */
    public static final String END_DOCUMENT = "EndDocument";

    /** Event name for the Start Element notification */
    public static final String START_ELEMENT = "StartElement";

    /** Event name for the End Element notification */
    public static final String END_ELEMENT = "EndElement";

    /** Event name for the character data notification */
    public static final String CHARACTER_DATA = "CharacterData";

    /** Event name for the whitespace notification */
    public static final String WHITESPACE = "Whitespace";

    /** State Transition definition that ignores a start document events */
    public static final StateTransitionDef IGNORE_START_DOCUMENT_DEF =
        new StateTransitionDef(START_DOCUMENT, StateDef.STAY_IN_STATE);

    /** State Transition definition that ignores an end document events */
    public static final StateTransitionDef IGNORE_END_DOCUMENT_DEF =
        new StateTransitionDef(END_DOCUMENT, StateDef.STAY_IN_STATE);

    /** State Transition definition that ignores whitespace events */
    public static final StateTransitionDef IGNORE_WHITESPACE_DEF =
        new StateTransitionDef(WHITESPACE, StateDef.STAY_IN_STATE);

    //
    // SAX Event Classes
    //

    /** Event generated from a SAX Start Element notification */
    public static final class StartElementEvent extends StateEvent {

        /** Serial ID */
        private static final long serialVersionUID = -4094456302599499937L;

        /** Element namespace URI */
        private String namespaceURI;

        /** Element local name */
        private String localName;

        /** Element qualified name (prefixed) */
        private String qName;

        /** Element attributes */
        private Attributes atts;

        /** Hide the constructor */
        private StartElementEvent() {
            super(START_ELEMENT);
        }

        /**
         * Returns the element namespace URI. The Namespace URI is required when
         * the namespaces property on the parser is set to true (the default),
         * and is optional when the namespaces property is false.
         */
        public final String getNamespaceURI() {
            return namespaceURI;
        }

        /**
         * Returns the element local name. The local name is required when the
         * namespaces property on the parser is set to true (the default), and
         * is optional when the namespaces property is false.
         */
        public final String getLocalName() {
            return localName;
        }

        /**
         * Returns the element qualified name (prefixed). the qualified name is
         * required when the namespace-prefixes property is true, and is
         * optional when the namespace-prefixes property is false (the default).
         */
        public final String getQualifiedName() {
            return qName;
        }

        /** Returns the element attributes */
        public final Attributes getAttributes() {
            return atts;
        }

        /** Returns the named attribute or <code>null</code> if not found */
        public final String getAttribute(final String attrName) {
            return atts.getValue(attrName);
        }

        /** Overloads parent method to render more detail on the event */
        public String toString() {
            final StringBuilder buf = new StringBuilder();
            buf.append(START_ELEMENT);
            buf.append(" <");
            buf.append(qName);
            for (int i = 0; i < atts.getLength(); i++) {
                buf.append(' ');
                buf.append(atts.getQName(i));
                buf.append("=\"");
                buf.append(atts.getValue(i));
                buf.append('"');
            }
            buf.append('>');
            return buf.toString();
        }
    } // StartElementEvent

    /** Event generated from a SAX End Element notification */
    public static class EndElementEvent extends StateEvent {

        /** Serial ID */
        private static final long serialVersionUID = 854969335865576832L;

        /** Element namespace URI */
        private String namespaceURI;

        /** Element local name */
        private String localName;

        /** Element qualified name (prefixed) */
        private String qName;

        /** Hide the constructor */
        private EndElementEvent() {
            super(END_ELEMENT);
        }

        /**
         * Returns the element namespace URI. The Namespace URI is required when
         * the namespaces property on the parser is set to true (the default),
         * and is optional when the namespaces property is false.
         */
        public final String getNamespaceURI() {
            return namespaceURI;
        }

        /**
         * Returns the element local name. The local name is required when the
         * namespaces property on the parser is set to true (the default), and
         * is optional when the namespaces property is false.
         */
        public final String getLocalName() {
            return localName;
        }

        /**
         * Returns the element qualified name (prefixed). the qualified name is
         * required when the namespace-prefixes property is true, and is
         * optional when the namespace-prefixes property is false (the default).
         */
        public final String getQualifiedName() {
            return qName;
        }

        /** Overloads parent method to render more detail on the event */
        public String toString() {
            final StringBuilder buf = new StringBuilder();
            buf.append(END_ELEMENT);
            buf.append(" </");
            buf.append(qName);
            buf.append('>');
            return buf.toString();
        }
    } // EndElementEvent

    /** Contains character data that is no purely white space */
    public static class CharacterDataEvent extends StateEvent {

        /** Serial ID */
        private static final long serialVersionUID = -1453397676443677356L;

        /** The character content */
        private String charData;

        /** Hide the constructor */
        private CharacterDataEvent() {
            super(CHARACTER_DATA);
        }

        /** Returns the character data */
        public final String getCharacterData() {
            return charData;
        }

        /** Overloads parent method to render more detail on the event */
        public String toString() {
            final StringBuilder buf = new StringBuilder();
            buf.append(CHARACTER_DATA);
            buf.append(" \"");
            buf.append(charData);
            buf.append('"');
            return buf.toString();
        }
    } // CharacterDataEvent

    /** Contains white space characters */
    public static class WhitespaceEvent extends StateEvent {

        /** Serial Version ID */
        private static final long serialVersionUID = 5974596029820836341L;

        /** The whitespace character data */
        private String charData;

        /** Hide the constructor */
        private WhitespaceEvent() {
            super(WHITESPACE);
        }

        /** Returns the character data */
        public final String getCharacterData() {
            return charData;
        }

        /** Overloads parent method to render more detail on the event */
        public String toString() {
            final StringBuilder buf = new StringBuilder();
            buf.append(WHITESPACE);
            buf.append(" \"");
            buf.append(charData);
            buf.append('"');
            return buf.toString();
        }
    }

    //
    // Shared SAX Event instances
    //

    /** SAX Start Document event */
    private static final StateEvent START_DOCUMENT_EVENT =
        new StateEvent(START_DOCUMENT);

    /** SAX End Document event */
    private static final StateEvent END_DOCUMENT_EVENT =
        new StateEvent(END_DOCUMENT);

    /** SAX Start Element event */
    private final StartElementEvent startElementEvent = new StartElementEvent();

    /** SAX End Element event */
    private final EndElementEvent endElementEvent = new EndElementEvent();

    /** SAX Character Data Event */
    private final CharacterDataEvent characterDataEvent =
        new CharacterDataEvent();

    /** Whitespace Event */
    private final WhitespaceEvent whitespaceEvent = new WhitespaceEvent();

    /** SAX Event Consumer */
    private final SerialStateTableControl stateTable;

    /**
     * Helper method that asserts the specified event is a start element event
     * with specified element qualified name.
     */
    public static StartElementEvent assertStartElement(
        final StateEvent event,
        final String qName) throws StateExeException {
        if (event instanceof StartElementEvent) {
            final StartElementEvent startEvent = (StartElementEvent) event;
            final String elementName = startEvent.getQualifiedName();
            if ((qName != null) && !qName.equals(elementName)) {
                throw new StateExeException("Expected start element, '" +
                    elementName +
                    "', to be '" +
                    qName +
                    "'");
            }
            return startEvent;
        } else {
            throw new StateExeException("Expected event, " +
                StartElementEvent.class.getName() +
                ", but found instead event, " +
                event.getClass().getName());
        }
    }

    /**
     * Helper method that asserts the specified event is a character data event
     * or whitespace and returns the trimmed character data. In the case of
     * whitespace,it returns an empty string.
     */
    public static String assertCharacterData(final StateEvent event)
        throws StateExeException {
        if (event instanceof CharacterDataEvent) {
            final CharacterDataEvent charDataEvent = (CharacterDataEvent) event;
            return charDataEvent.getCharacterData();
        } else if (event instanceof WhitespaceEvent) {
            final WhitespaceEvent whitespaceEvent = (WhitespaceEvent) event;
            return whitespaceEvent.getCharacterData();
        } else {
            throw new StateExeException("Expected event, " +
                CharacterDataEvent.class.getName() +
                ", but found instead event, " +
                event.getClass().getName());
        }
    }

    /**
     * Construct with a state table that does not use separate threads
     * 
     * @throws StateExeException thrown when the state table does not properly
     *         start
     */
    public SaxEventAdapter(final SerialStateTableControl stateTable)
        throws StateExeException {
        this.stateTable = stateTable;
        this.stateTable.start();
    }

    //
    // Content Handler
    //

    /**
     * Receive notification of the beginning of a document. The SAX parser will
     * invoke this method only once, before any other methods.
     */
    public void startDocument() throws SAXException {
        try {
            stateTable.signalEvent(START_DOCUMENT_EVENT);
        } catch (Exception exc) {
            SAXException saxExc =
                new SAXException("State table exception", exc);
            saxExc.initCause(exc);
            throw saxExc;
        }
    }

    /**
     * Receive notification of the end of a document. The SAX parser will invoke
     * this method only once, and it will be the last method invoked during the
     * parse. The parser shall not invoke this method until it has either
     * abandoned parsing (because of an unrecoverable error) or reached the end
     * of input.
     */
    public void endDocument() throws SAXException {
        try {
            stateTable.signalEvent(END_DOCUMENT_EVENT);
        } catch (Exception exc) {
            SAXException saxExc =
                new SAXException("State table exception", exc);
            saxExc.initCause(exc);
            throw saxExc;
        }
    }

    /**
     * Receive notification of the beginning of an element. The Parser will
     * invoke this method at the beginning of every element in the XML document;
     * there will be a corresponding endElement event for every startElement
     * event (even when the element is empty). All of the element's content will
     * be reported, in order, before the corresponding endElement event.
     * <p>
     * This event allows up to three name components for each element:
     * <ul>
     * <li>the Namespace URI
     * <li>the local name
     * <li>the qualified (prefixed) name
     * </ul>
     * Any or all of these may be provided, depending on the values of the
     * http://xml.org/sax/features/namespaces and the
     * http://xml.org/sax/features/namespace-prefixes properties:<br>
     * the Namespace URI and local name are required when the namespaces
     * property is true (the default), and are optional when the namespaces
     * property is false (if one is specified, both must be); the qualified name
     * is required when the namespace-prefixes property is true, and is optional
     * when the namespace-prefixes property is false (the default).
     * 
     * @param namespaceURI the element namespace URI
     * @param localName the element local name
     * @param qName the element qualified (prefixed) name
     * @param atts attributes on the element
     */
    public void startElement(
        final String namespaceURI,
        final String localName,
        final String qName,
        final Attributes atts) throws SAXException {

        // Set the event state
        startElementEvent.namespaceURI = namespaceURI;
        startElementEvent.localName = localName;
        startElementEvent.qName = qName;
        startElementEvent.atts = atts;

        // Signal the event to the state table
        try {
            stateTable.signalEvent(startElementEvent);
        } catch (Exception exc) {
            SAXException saxExc =
                new SAXException("State table exception", exc);
            saxExc.initCause(exc);
            throw saxExc;
        }
    }

    /**
     * Receive notification of the end of an element. The SAX parser will invoke
     * this method at the end of every element in the XML document; there will
     * be a corresponding startElement event for every endElement event (even
     * when the element is empty).
     * 
     * @param namespaceURI the element namespace URI
     * @param localName the element local name
     * @param qName the element qualified (prefixed) name
     */
    public void endElement(
        final String namespaceURI,
        final String localName,
        final String qName) throws SAXException {
        // Set the event state
        endElementEvent.namespaceURI = namespaceURI;
        endElementEvent.localName = localName;
        endElementEvent.qName = qName;

        // Signal the event to the state table
        try {
            stateTable.signalEvent(endElementEvent);
        } catch (Exception exc) {
            SAXException saxExc =
                new SAXException("State table exception", exc);
            saxExc.initCause(exc);
            throw saxExc;
        }
    }

    /**
     * Receive notification of character data. The Parser will call this method
     * to report each chunk of character data. SAX parsers may return all
     * contiguous character data in a single chunk, or they may split it into
     * several chunks; however, all of the characters in any single event must
     * come from the same external entity so that the Locator provides useful
     * information.
     */
    public void characters(final char[] ch, final int start, final int length)
        throws SAXException {
        final String chars = new String(ch, start, length);
        try {
            if (isWhitespace(chars)) {

                // Signal this event if all the characters are whitespace
                whitespaceEvent.charData = chars;
                stateTable.signalEvent(whitespaceEvent);
            } else {

                // Signal this event if at least some characters are not
                // whitespace
                characterDataEvent.charData = chars;
                stateTable.signalEvent(characterDataEvent);
            }
        } catch (Exception exc) {
            SAXException saxExc =
                new SAXException("State table exception", exc);
            saxExc.initCause(exc);
            throw saxExc;
        }
    }

    /**
     * Receive notification of a recoverable error.
     * <p>
     * This corresponds to the definition of "error" in section 1.2 of the W3C
     * XML 1.0 Recommendation. For example, a validating parser would use this
     * callback to report the violation of a validity constraint. This method
     * throws the received exception.
     */
    public void error(final SAXParseException exception) throws SAXException {
        throw exception;
    }

    /**
     * Receive notification of a non-recoverable error.
     * <p>
     * This corresponds to the definition of "fatal error" in section 1.2 of the
     * W3C XML 1.0 Recommendation. For example, a parser would use this callback
     * to report the violation of a well-formedness constraint. This method
     * throws the received exception.
     */
    public void fatalError(final SAXParseException exception)
        throws SAXException {
        throw exception;
    }
    
    /** Returns <code>true</code> when the provided string consists only of white space */
    private static boolean isWhitespace(final String chars) {
    	for (int i = 0; i < chars.length(); i++) {
    		final char chr = chars.charAt(i);
    		if (!Character.isWhitespace(chr)) {
    			return false;
    		}
    	}
    	return true;
    }
}
